package com.client.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.header.HeaderWriter;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;

import static org.springframework.security.config.Customizer.withDefaults;

/**
 * @author chengyadong
 * @date 2023/11/28 10:18
 * @description 客户端配置类
 * 客户端所有的请求都走拦截，不管哪种模式都需要去授权服务器(服务提供商)验证，验证通过后授权服务器返回给token，然后客户端拿着token去访问资源服务器
 * 注意：主配置类解析完成后，先会对import的生成bd,然后@bean的在生成bd 放入到List<String> beanDefinitionNames集合中，然后循环遍历生成bean对象，所以
 * EnableWebSecurity注解引入的import会先生成bean对象被触发 注意import引入的@bean对象会在当前配置类引入的@bean对象之后执行，除非被需要注入
 * EnableWebSecurity通过import导入了WebSecurityConfiguration类，在该类WebSecurityConfiguration bean生命周期过程中又通过@Autowired(required = false)
 * 修饰的方法依赖注入了SecurityFilterChain，WebSecurityCustomizer接口的实现类，所以ClientServiceConfig配置类对应的两个bean对象会在WebSecurityConfiguration
 * 的bean对象之前生成
 */
@Configuration
@EnableWebSecurity(debug = true)
public class ClientServiceConfig {
	//三个比较重要的@bean修饰方法执行顺序：HttpSecurity->SecurityFilterChain->springSecurityFilterChain
	@Bean
	//此处也注入了HttpSecurity对象，其是一个原型,再其bean方法执行的过程中会帮我们创建一些默认的configur,所以一些配置我们不需要定义仍然可以使用，比如HeadersConfigurer，
	//SessionManagementConfigurer等
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		// 注意这里的authorizeHttpRequests即为添加的配置类AuthorizeHttpRequestsConfigurer，通过lambda表达式往该配置类中初始化属性信息
		// 另外解析一下下面的三行配置的用处：每一行都是一个路径+权限 requestMatchers("/js/**", "/css/**")配置一个路径匹配器 permitAll()
		// 会通过access方法增加一个AuthorizationManager授权管理器的实现类，来处理当前匹配的路径的授权处理
		// 第二行会通过hasRole增加授权管理器  第三行通过authenticated方法增加授权管理器
		// 增加的授权管理器最终会在AuthorizationFilter(授权处理，1:未认证时访问客户端资源跳转到授权服务器也是在该过滤器中抛出异常，重定向到授权服务器的
		// 2:认证完成后，该用户是否有权限访问资源，比如角色是否符合  均是在该过滤器完成的)中使用
		http.authorizeHttpRequests((authorizeHttpRequests) ->
				// 当请求路径匹配到 /js/** , /css/** 这类静态资源的时候，不做认证，直接放行,其他请求进行拦截
				authorizeHttpRequests.requestMatchers("/js/**", "/css/**").permitAll()
						// 该请求必须具有admin角色才可以访问
						.requestMatchers("/helloTest/query").hasAuthority("SCOPE_write")
						// 被“remember me”的用户允许访问 这个有点类似于很多网站的十天内免登录，登陆一次即可记住你，然后未来一段时间不用登录
						.requestMatchers("/test").rememberMe()
						// anonymous()表示可以匿名访问匹配的URL。和permitAll()效果类似
						.requestMatchers("/test1").anonymous()
						// 如果用户不是被 remember me 的，才可以访问。也就是必须一步一步按部就班的登录才行
						.requestMatchers("/test2").fullyAuthenticated()
						.anyRequest().authenticated()
		);
		//设置当前http构造的SecurityFilterChain中的过滤器链匹配哪种样式的请求
		http.securityMatcher(AnyRequestMatcher.INSTANCE);
		//用于配置OAuth 2.0登录，它包括身份验证和授权过程--OAuth2AuthorizationRequestRedirectFilter OAuth2LoginAuthenticationFilter(/login/oauth2/code/)
		http.oauth2Login(withDefaults());
		//主要关注于客户端的配置，例如客户端ID、客户端密钥等--OAuth2AuthorizationRequestRedirectFilter OAuth2AuthorizationCodeGrantFilter(/login/oauth2/code/)
		http.oauth2Client(withDefaults());
		//logout和cors配与不配都一样，参数http中已经默认带了这两个Configurer 那我们在这里使用http.logout岂不是无用？ 其实并不是，虽然Configurer帮我们配置了
		//但是我们可以通过该方式来配置Configurer中的属性信息，从而将配置传递到filter中   只不过我们不需要再创建Configurer而已直接使用已经存在的
		http.logout(withDefaults());
		//前后端分离时关闭跨域,注意：corsFilter是spring统一提供的
		http.cors(Customizer.withDefaults());
		//可以给当前请求添加一些头信息 比如：启用浏览器保护。像X-Frame-Options, X-XSS-Protection和X-Content-Type-Options
		/*http.headers((header) -> header.addHeaderWriter(setHeadersInfo()));*/
		//构造SecurityFilterChain(最终返回一个DefaultSecurityFilterChain,里面封装了一系列已经初始化完成的filter)
		return http.build();
	}

	//设置X-XSS-Protection，每种头信息都对应一个对象，该对象均实现了HeaderWriter接口
	private HeaderWriter setHeadersInfo() {
		XXssProtectionHeaderWriter t = new XXssProtectionHeaderWriter();
		t.setHeaderValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED);
		return t;
	}

	/**
	 * 开放一些端点的访问控制 不需要认证就可以访问的端口 针对的是webSecurity
	 *
	 * @return
	 */
	@Bean
	public WebSecurityCustomizer ignoringCustomizer() {
		return (web) -> web.ignoring().requestMatchers("/ignore3", "/ignore4");
	}

	//自定义webSecurity防火墙
/*	@Bean
	public WebSecurityCustomizer httpFireWallCustomizer() {
		return (web) -> web.httpFirewall(httpFirewall());
	}*/

	//使用系统自带的StrictHttpFirewall防火墙，只是对其属性进行调整
	HttpFirewall httpFirewall() {
		StrictHttpFirewall firewall = new StrictHttpFirewall();
		//不做HTTP请求方法校验，也就是什么方法都可以过
		firewall.setUnsafeAllowAnyHttpMethod(true);
		//默认情况下，springSecurity中的请求不能带； 如果出现就会异常抛出  通过该设置可以带；
		firewall.setAllowSemicolon(true);
		// 如果请求地址中出现双斜杠，这个请求也将被拒绝。双斜杠//使用URL地址编码之后，是%2F%2F，其中F大小写无所谓，
		// 所以请求地址中也能不出现 "%2f%2f", "%2f%2F", "%2F%2f", "%2F%2F"
		// 如果希望请求地址中可以出现 // 可以通过该方式进行设置
		firewall.setAllowUrlEncodedDoubleSlash(true);
		//如果请求地址中出现%，这个请求也将被拒绝。URL编码后的%是%25，所以%25也不能出现在URL地址中 可以通过该方式进行设置
		firewall.setAllowUrlEncodedPercent(true);
		//如果请求地址中包含反斜杠\或者反斜杠编码后的字符 %5C 或者 %5c,则请求将被拒绝。  可以通过该方式进行设置
		firewall.setAllowBackSlash(true);
		//如果请求地址中包含斜杠编码后的字符 %2F 或者 %2f ，则请求将被拒绝 可以通过该方式进行设置
		firewall.setAllowUrlEncodedSlash(true);
		//如果请求地址中存在 . 编码之后的字符 %2e、%2E，则请求将被拒绝
		firewall.setAllowUrlEncodedPeriod(true);
		return firewall;
	}

}
